-
-
Notifications
You must be signed in to change notification settings - Fork 21.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve Scene Tree editor performance #99700
base: master
Are you sure you want to change the base?
Conversation
Before: (this scene has about 15,000 nodes) Screencast.From.2024-11-26.02-11-22.mp4After: Screencast.From.2024-11-26.02-10-35.mp4 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stealing @AThousandShips 's job 🤣
6aefc0b
to
9a25be0
Compare
431094f
to
05519d4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I confirm that birdies are in the nest (dots at the end of the comments)😃
05519d4
to
d4d55d0
Compare
Changing valid types does not update the nodes (they stay disabled/enabled). Can be tested with this simple EditorScript: extends EditorScript
func _run() -> void:
EditorInterface.popup_node_selector(func(n): print(n), ["Node2D"]) Run it, then remove "Node2D" and run again. Nodes that didn't match originally will stay disabled. |
c5fca34
to
e076f60
Compare
This comment was marked as outdated.
This comment was marked as outdated.
e076f60
to
8851ae1
Compare
9a856ff
to
8d7b5eb
Compare
@KoBeWi I think I addressed your issues. Part of the reason I didn't figure it out was that there's a bug in master already whereby in the reparent node dialog nodes with disabled processing wouldn't show up as red. This is also fixed in this PR now. The following things were tested: old tests:
new tests:
|
Found another bug with node selector. |
I checked the implementation and looks like the main idea is hashing everything relevant for node appearance and using it to update only when needed. This means that every time we add something new that can affect anything about TreeItems we need to add it to the hash; rather error-prone, but the improved performance makes this compromise reasonable. There are still a couple of bugged cases, but other than that I think the PR is fine overall. |
As for changing the hash, I agree that it is a bit more work but on the other hand, for new things added it not showing up in the editor should be immediately apparent, unlike now where we are adding it now. The things checked outside of the hash are things that are related to the editor state, not the node state. I didn't want to have to pass variables into the _hash_node() method. I could make it work that way if you'd like? It'd be pretty much the same amount of code. For the pre_filter_color, that cannot be part of the hash as it is something that happens after nodes are updated and is also purely editor state.
I think that is all you found this round, right? |
Yes, last time I just checked all code that sets custom color or adds buttons, but there are other ways how TreeItems can be changed. There might be some tooltip changes I missed.
If you need to pass more than TreeItem then it's not worth it. |
I'll go through _update_tooltip with a fine-tooth comb to make sure I have indeed caught everything.
I would have to, yes. I'll leave it as-is then. |
c3a3a60
to
3c006fe
Compare
@KoBeWi I think I addressed your concerns and fixed the remaining bugs. I've also made the hash function smaller by relying on signals to update some aspects of the editor. These signals get emitted by Node itself, they must also be emitted if a script changes them. This claws back quite a lot of performance compared to the original non-hash based version. Please be aware that quite a bit changed compared to last time! The following things were tested:
new tests:
|
3c006fe
to
3dc7dea
Compare
I think I see a couple more ways to optimize things a bit further. I'll make more changes, sorry for the extra noise. |
Nice! I had been working on a similar change but with the holidays had to put it down. I'll go through this pr sometime this weekend and see if there are any changes which could be brought over. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall good changes! I feel skeptical about the value provided by some of the hashing/tree level dirty marking when we have granular access into specific node level changes. It feels like the old code was in part as prone to being slow as it was because the logic was so spread out that it was easiest and most reliable to fall back to a full tree update, and while the new code solves the full tree update part it retains IMO a lot of the complexity of the old code. Maybe I just missed some cases that required that approach.
Separately, on the core channel before we talked briefly about the sort of bad caching done within TreeItem
and how that could be improved potentially with a skip list. Have you started on / planned to make those changes? If not, my branch also includes some other pretty minor improvements which I could rebase against this.
editor/gui/scene_tree_editor.cpp
Outdated
break; | ||
} | ||
} | ||
if (I) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this quit once !I
since there are node parents outside of the editable tree but none past the editable root will have tree items? and more generally this can walk the tree items themselves rather than indexing into the map if the code applies any node added/removed handling prior
editor/gui/scene_tree_editor.cpp
Outdated
if (IC) { | ||
TreeItem *item = IC->value.item; | ||
|
||
if (IC->value.item->get_parent() != parent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a reason you do this here rather than when node removed fires? it seems like it would simplify the code to just handle reordering here
@@ -847,6 +1079,7 @@ void SceneTreeEditor::_test_update_tree() { | |||
if (get_scene_node()) { | |||
_compute_hash(get_scene_node(), hash); | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it worth keeping this and the _tree_changed
handler around given the added/removed/rearranged callbacks give us the specific node/subtree and change? What case would fail for those other callbacks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new _tree_changed handler only dives into subtrees of changed nodes. This should fix your concern.
_update_tree() now doesn't much more work than is required.
tree->clear(); | ||
node_cache.clear(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is repeated; it might be worth adding a reset method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add a reset method.
editor/gui/scene_tree_editor.cpp
Outdated
TreeItem *item = I->value.item; | ||
TreeItem *parent = item->get_parent(); | ||
if (parent) { | ||
parent->remove_child(item); | ||
} | ||
node_cache.remove(I); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks like it leaks item
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, I fixed this in this iteration.
3dc7dea
to
22b2b64
Compare
@a-johnston @KoBeWi I've changed the way the updating system works, it is simpler now. As discussed it adds a new signal to node that fires only in the editor to make sure everything stays up to date. I think this addresses everyone's concerns. Note that there's a couple small issues left, which I will fix in a next push, assuming this design doesn't get shot down:
all of the other tests shown above work. |
Yeah, I realized this also. This now only updates the sub trees that were affected, but starting from the root of the tree each time, not updating any branches without dirty nodes in them. The reason for starting from "the top" every time is so I could retain all of the surrounding logic of _update_tree, and because basically every time you do anything you have to dirty the parent as well. Updating a small handful of extra parents that might not need it doesn't seem like a big problem.
That was what I started on, but the Tree optimizations didn't really touch the performance issues of SceneTreeEditor. I do want to improve that as well, but in a separate PR. There's some optimizations I want to do to List<> and then move Tree to use List instead. That doesn't solve all problems, but some at least. |
We now cache the Node*<>TreeItem* mapping in the SceneTreeEditor. This allows us to make targeted updates to the Tree used to display the scene tree in the editor. Previously on almost all changes to the scene tree the editor would rebuild the entire widget, causing a large number of deallocations an allocations. We now carefully manipulate the Tree widget in-situ saving a large number of these allocations. There is definitely more that could be done, but this is already a massive improvement. This fixes godotengine#83460
Since this function now exists there seems to be little reason not to expose it. Discussed with @bruvzg.
22b2b64
to
377141a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like editor_state_changed
change, I think it's a good way of hooking into this without adding more special cases.
The reason for starting from "the top" every time is so I could retain all of the surrounding logic of _update_tree, and because basically every time you do anything you have to dirty the parent as well. Updating a small handful of extra parents that might not need it doesn't seem like a big problem.
That's a good enough reason for me. I think I just didn't like the idea of needing that linear pass of each layer to re-find which nodes are dirty when we started with that information, but, even if there are ways around that, you've already been testing this against the worst case scenario scene tree for that cost and it's fine.
item->set_text_overrun_behavior(0, TextServer::OVERRUN_NO_TRIMMING); | ||
if (p_parent && dirty) { | ||
bool reparent = false; | ||
int current_node_index = p_node->get_index(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it intentional not including internal children here and in other spots? I noticed the original code always included internal children but here we omit them sometimes, but not always, and even mix and match inclusion such as in setting child_index
below
|
||
} else { | ||
// A parent might have moved/renamed. | ||
item->set_metadata(0, p_node->get_path()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it would be worthwhile changing this metadata to be the Node *
value itself rather than than the path? I did it on my branch but I don't know if it actually noticeably helped performance. Most locations end up just passing the value to get_node
and the remaining use is _find
which I think could be removed entirely. It would also of course avoid needing to update the path when that's the only change.
TreeItem *p = I->value.item->get_parent(); | ||
if (p) { | ||
p->remove_child(I->value.item); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the memdelete
call handles this
I don't know why I didn't remember/mention this earlier but it might also be interesting to stage these removed TreeItems for deletion but not free them immediately so that when nodes are moved between subtrees the same TreeItem can be reused to avoid an alloc. I suppose it would be updated anyways though and at that point it's basically just instance pooling.
// Editor only signal to keep the SceneTreeEditor in sync. | ||
void _emit_editor_state_changed(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you may also do this and keep the definition in node.cpp
fully surrounded by TOOLS_ENABLED:
// Editor only signal to keep the SceneTreeEditor in sync. | |
void _emit_editor_state_changed(); | |
#ifdef TOOLS_ENABLED | |
void _emit_editor_state_changed(); | |
#else | |
void _emit_editor_state_changed() {}; | |
#endif |
But it's like, whatever. Same thing.
#ifdef TOOLS_ENABLED | ||
// This is required for the SceneTreeEditor to properly keep track of when an update is needed. | ||
// This signal might be expensive and not needed for anything outside of the editor. | ||
if (Engine::get_singleton()->is_editor_hint()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A notable concern to me is that editor_state_changed
will emit regardless of whether the node belongs in an edited scene or not. And this could be fixed...
if (Engine::get_singleton()->is_editor_hint()) { | |
if (is_part_of_edited_scene()) { |
But at the same time, nothing of the signal hints that it would only emit these nodes specifically.
@@ -1068,6 +1068,11 @@ | |||
Emitted when the node's editor description field changed. | |||
</description> | |||
</signal> | |||
<signal name="editor_state_changed"> | |||
<description> | |||
Emitted when field relevant to the editor changed. Only emitted in the editor, to update the scene tree. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once this is finalized, and if this signal would still exist... this is not good. We need to be more specific. For now:
Emitted when field relevant to the editor changed. Only emitted in the editor, to update the scene tree. | |
Emitted when an attribute of the node that is relevant to the editor changed. Only emitted in the editor. |
We now cache the Node*<>TreeItem* mapping in the SceneTreeEditor. This allows us to make targeted updates to the Tree used to display the scene tree in the editor.
Previously on almost all changes to the scene tree the editor would rebuild the entire widget, causing a large number of deallocations an allocations. We now carefully manipulate the Tree widget in-situ saving a large number of these allocations.
There is definitely more that could be done, but this is already a massive improvement.
This fixes #83460